﻿//******************************************************************************************************
//  FacileActionAdapterBase.cs - Gbtc
//
//  Copyright © 2012, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
//  not use this file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://www.opensource.org/licenses/eclipse-1.0.php
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  09/02/2010 - J. Ritchie Carroll
//       Generated original version of source code.
//  12/02/2010 - J. Ritchie Carroll
//       Added an immediate measurement tracking option for incoming data.
//  11/01/2013 - Stephen C. Wills
//       Updated to process time-series entities.
//
//******************************************************************************************************

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using GSF.TimeSeries.Routing;

namespace GSF.TimeSeries.Adapters
{
    /// <summary>
    /// Represents the base class for simple, non-time-aligned, action adapters.
    /// </summary>
    /// <remarks>
    /// This base class acts on incoming time-series entities, in a non-time-aligned fashion, for general processing.
    /// If derived class needs time-aligned data for processing, the <see cref="ActionAdapterBase"/> class should be used instead.
    /// Derived classes are expected call <see cref="OnNewEntities"/> for any new entities that may get created.
    /// </remarks>
    public abstract class FacileActionAdapterBase : AdapterBase, IActionAdapter
    {
        #region [ Members ]

        // Events

        /// <summary>
        /// Provides new time-series entities from action adapter.
        /// </summary>
        /// <remarks>
        /// <see cref="EventArgs{T}.Argument"/> is a collection of new entities for host to process.
        /// </remarks>
        public event EventHandler<RoutingEventArgs> NewEntities;

        /// <summary>
        /// This event is raised by derived class, if needed, to track current number of unpublished seconds of data in the queue.
        /// </summary>
        /// <remarks>
        /// <see cref="EventArgs{T}.Argument"/> is the total number of unpublished seconds of data.
        /// </remarks>
        public event EventHandler<EventArgs<int>> UnpublishedSamples;

        // Fields
        private List<string> m_inputSourceIDs;
        private List<string> m_outputSourceIDs;
        private ISet<Guid> m_requestedInputSignals;
        private ISet<Guid> m_requestedOutputSignals;
        private bool m_respectInputDemands;
        private bool m_respectOutputDemands;
        private RoutingEventArgs m_routingEventArgs;
        private readonly object m_newEntitiesLock;
        private double m_lagTime;
        private double m_leadTime;
        private int m_framesPerSecond;                                          // Defined frames per second, if defined
        private bool m_trackLatestEntities;                                     // Determines whether or not to track latest time-series entities
        private readonly IDictionary<Guid, ITimeSeriesEntity> m_latestEntities; // Absolute latest received time-series entities
        private bool m_useLocalClockAsRealTime;                                 // Determines whether or not to use local system clock as "real-time"
        private long m_realTimeTicks;                                           // Timestamp of real-time or the most recently received time-series entity

        #endregion

        #region [ Constructors ]

        /// <summary>
        /// Creates a new instance of the <see cref="FacileActionAdapterBase"/> class.
        /// </summary>
        protected FacileActionAdapterBase()
        {
            m_newEntitiesLock = new object();
            m_latestEntities = new Dictionary<Guid, ITimeSeriesEntity>();
            m_useLocalClockAsRealTime = true;
        }

        #endregion

        #region [ Properties ]

        /// <summary>
        /// Gets or sets primary keys of input signals the <see cref="FacileActionAdapterBase"/> expects, if any.
        /// </summary>
        [ConnectionStringParameter,
        DefaultValue(null),
        Description("Defines primary keys of input signals the adapter expects; can be one of a filter expression, measurement key, point tag or Guid."),
        CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")]
        public override ISet<Guid> InputSignalIDs
        {
            get
            {
                return base.InputSignalIDs;
            }
        }

        /// <summary>
        /// Gets or sets <see cref="MeasurementKey.Source"/> values used to filter input signals.
        /// </summary>
        /// <remarks>
        /// This allows an adapter to associate itself with entire collections of signals based on the source of the measurement keys.
        /// Set to <c>null</c> apply no filter.
        /// </remarks>
        public virtual string[] InputSourceIDs
        {
            get
            {
                if (m_inputSourceIDs == null)
                    return null;

                return m_inputSourceIDs.ToArray();
            }
            set
            {
                if (value == null)
                {
                    m_inputSourceIDs = null;
                }
                else
                {
                    m_inputSourceIDs = new List<string>(value);
                    m_inputSourceIDs.Sort();
                }

                // Filter signals to list of specified source IDs
                LoadInputSourceIDs(this);
            }
        }

        /// <summary>
        /// Gets or sets <see cref="MeasurementKey.Source"/> values used to filter output signals.
        /// </summary>
        /// <remarks>
        /// This allows an adapter to associate itself with entire collections of signals based on the source of the measurement keys.
        /// Set to <c>null</c> apply no filter.
        /// </remarks>
        public virtual string[] OutputSourceIDs
        {
            get
            {
                if (m_outputSourceIDs == null)
                    return null;

                return m_outputSourceIDs.ToArray();
            }
            set
            {
                if (value == null)
                {
                    m_outputSourceIDs = null;
                }
                else
                {
                    m_outputSourceIDs = new List<string>(value);
                    m_outputSourceIDs.Sort();
                }

                // Filter signals to list of specified source IDs
                LoadOutputSourceIDs(this);
            }
        }

        /// <summary>
        /// Gets or sets input signals that are requested by other adapters based on what adapter says it can provide.
        /// </summary>
        public virtual ISet<Guid> RequestedInputSignals
        {
            get
            {
                return m_requestedInputSignals;
            }
            set
            {
                m_requestedInputSignals = value;
            }
        }

        /// <summary>
        /// Gets or sets output signals that are requested by other adapters based on what adapter says it can provide.
        /// </summary>
        public virtual ISet<Guid> RequestedOutputSignals
        {
            get
            {
                return m_requestedOutputSignals;
            }
            set
            {
                m_requestedOutputSignals = value;
            }
        }

        /// <summary>
        /// Gets or sets flag indicating if action adapter should respect auto-start requests based on input demands.
        /// </summary>
        /// <remarks>
        /// Action adapters are in the curious position of being able to both consume and produce points, as such the user needs to be able to control how their
        /// adapter will behave concerning routing demands when the adapter is setup to connect on demand. In the case of respecting auto-start input demands,
        /// as an example, this would be <c>false</c> for an action adapter that calculated measurements, but <c>true</c> for an action adapter used to archive inputs.
        /// </remarks>
        public virtual bool RespectInputDemands
        {
            get
            {
                return m_respectInputDemands;
            }
            set
            {
                m_respectInputDemands = value;
            }
        }

        /// <summary>
        /// Gets or sets flag indicating if action adapter should respect auto-start requests based on output demands.
        /// </summary>
        /// <remarks>
        /// Action adapters are in the curious position of being able to both consume and produce points, as such the user needs to be able to control how their
        /// adapter will behave concerning routing demands when the adapter is setup to connect on demand. In the case of respecting auto-start output demands,
        /// as an example, this would be <c>true</c> for an action adapter that calculated measurements, but <c>false</c> for an action adapter used to archive inputs.
        /// </remarks>
        public virtual bool RespectOutputDemands
        {
            get
            {
                return m_respectOutputDemands;
            }
            set
            {
                m_respectOutputDemands = value;
            }
        }

        /// <summary>
        /// Gets or sets the frames per second to be used by the <see cref="FacileActionAdapterBase"/>.
        /// </summary>
        /// <remarks>
        /// This value is only tracked in the <see cref="FacileActionAdapterBase"/>, derived class will determine its use.
        /// </remarks>
        [ConnectionStringParameter,
        DefaultValue(0),
        Description("Defines the number of frames per second expected by the adapter.")]
        public virtual int FramesPerSecond
        {
            get
            {
                return m_framesPerSecond;
            }
            set
            {
                m_framesPerSecond = value;
            }
        }

        /// <summary>
        /// Gets or sets the allowed past time deviation tolerance, in seconds (can be sub-second).
        /// </summary>
        /// <remarks>
        /// <para>Defines the time sensitivity to past timestamps.</para>
        /// <para>The number of seconds allowed before assuming a timestamp is too old.</para>
        /// <para>This becomes the amount of delay introduced by the concentrator to allow time for data to flow into the system.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">LagTime must be greater than zero, but it can be less than one.</exception>
        [ConnectionStringParameter,
        DefaultValue(10.0D),
        Description("Defines the allowed past time deviation tolerance, in seconds (can be sub-second).")]
        public double LagTime
        {
            get
            {
                return m_lagTime;
            }
            set
            {
                m_lagTime = value;
            }
        }

        /// <summary>
        /// Gets or sets the allowed future time deviation tolerance, in seconds (can be sub-second).
        /// </summary>
        /// <remarks>
        /// <para>Defines the time sensitivity to future timestamps.</para>
        /// <para>The number of seconds allowed before assuming a timestamp is too advanced.</para>
        /// <para>This becomes the tolerated +/- accuracy of the local clock to real-time.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">LeadTime must be greater than zero, but it can be less than one.</exception>
        [ConnectionStringParameter,
        DefaultValue(5.0D),
        Description("Defines the allowed future time deviation tolerance, in seconds (can be sub-second).")]
        public double LeadTime
        {
            get
            {
                return m_leadTime;
            }
            set
            {
                m_leadTime = value;
            }
        }

        /// <summary>
        /// Gets or sets flag to start tracking the absolute latest received time-series entities.
        /// </summary>
        /// <remarks>
        /// Latest received time-series entity will be available via the <see cref="LatestEntities"/> property.
        /// </remarks>
        public virtual bool TrackLatestEntities
        {
            get
            {
                return m_trackLatestEntities;
            }
            set
            {
                m_trackLatestEntities = value;
            }
        }

        /// <summary>
        /// Gets reference to the collection of absolute latest received time-series entities.
        /// </summary>
        public virtual IList<ITimeSeriesEntity> LatestEntities
        {
            get
            {
                return m_latestEntities.Values.ToList();
            }
        }

        /// <summary>
        /// Gets or sets flag that determines whether or not to use the local clock time as real time.
        /// </summary>
        /// <remarks>
        /// Use your local system clock as real time only if the time is locally GPS-synchronized,
        /// or if the time-series entities being sorted were not measured relative to a GPS-synchronized clock.
        /// Turn this off if the class is intended to process historical data.
        /// </remarks>
        public virtual bool UseLocalClockAsRealTime
        {
            get
            {
                return m_useLocalClockAsRealTime;
            }
            set
            {
                m_useLocalClockAsRealTime = value;
            }
        }

        /// <summary>
        /// Gets the the most accurate time value that is available. If <see cref="UseLocalClockAsRealTime"/> = <c>true</c>, then
        /// this function will return <see cref="DateTime.UtcNow"/>. Otherwise, this function will return the timestamp of the
        /// most recent time-series entity.
        /// </summary>
        public Ticks RealTime
        {
            get
            {
                // When using local clock as real-time, assume this is the best value we have for real time.
                if (UseLocalClockAsRealTime || !TrackLatestEntities)
                    return DateTime.UtcNow.Ticks;

                // Assume latest timestamp is the best value we have for real-time.
                return m_realTimeTicks;
            }
        }

        /// <summary>
        /// Returns the detailed status of the data input source.
        /// </summary>
        /// <remarks>
        /// Derived classes should extend status with implementation specific information.
        /// </remarks>
        public override string Status
        {
            get
            {
                StringBuilder status = new StringBuilder();

                status.Append(base.Status);
                status.AppendFormat("        Defined frame rate: {0} frames/sec", FramesPerSecond);
                status.AppendLine();
                status.AppendFormat("           Entity tracking: {0}", m_trackLatestEntities ? "Enabled" : "Disabled");
                status.AppendLine();
                status.AppendFormat("  Respecting input demands: {0}", RespectInputDemands);
                status.AppendLine();
                status.AppendFormat(" Respecting output demands: {0}", RespectOutputDemands);
                status.AppendLine();

                return status.ToString();
            }
        }

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Initializes <see cref="FacileActionAdapterBase"/>.
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();

            Dictionary<string, string> settings = Settings;
            string setting;

            if (settings.TryGetValue("framesPerSecond", out setting))
                FramesPerSecond = int.Parse(setting);

            if (settings.TryGetValue("useLocalClockAsRealTime", out setting))
                UseLocalClockAsRealTime = setting.ParseBoolean();

            if (settings.TryGetValue("trackLatestEntities", out setting))
                TrackLatestEntities = setting.ParseBoolean();

            if (TrackLatestEntities)
            {
                if (settings.TryGetValue("lagTime", out setting))
                    LagTime = double.Parse(setting);
                else
                    LagTime = 10.0;

                if (settings.TryGetValue("leadTime", out setting))
                    LeadTime = double.Parse(setting);
                else
                    LeadTime = 5.0;
            }

            if (settings.TryGetValue("respectInputDemands", out setting))
                RespectInputDemands = setting.ParseBoolean();
            else
                RespectInputDemands = false;

            if (settings.TryGetValue("respectOutputDemands", out setting))
                RespectOutputDemands = setting.ParseBoolean();
            else
                RespectOutputDemands = true;
        }

        /// <summary>
        /// Queues a collection of time-series entities for processing.
        /// </summary>
        /// <param name="entities">Entities to queue for processing.</param>
        [TimeSeriesProcessingMethod]
        public void QueueEntitiesForProcessing(IEnumerable<ITimeSeriesEntity> entities)
        {
            ITimeSeriesEntity latestEntity;

            // If enabled, facile adapter will track the absolute latest time-series entities.
            if (m_trackLatestEntities)
            {
                bool useLocalClockAsRealTime = UseLocalClockAsRealTime;

                foreach (ITimeSeriesEntity entity in entities)
                {
                    // If this entity is more recent than the latest entity for the same signal, make this entity the latest entity
                    if (!m_latestEntities.TryGetValue(entity.ID, out latestEntity) || entity.Timestamp > latestEntity.Timestamp)
                        m_latestEntities[entity.ID] = entity;

                    // Track latest timestamp as real-time, if requested.
                    // This class is not currently going through hassle of determining if
                    // the latest timestamp is reasonable...
                    if (!useLocalClockAsRealTime && entity.Timestamp > m_realTimeTicks)
                        m_realTimeTicks = entity.Timestamp;
                }
            }

            ProcessEntities(entities);
        }

        /// <summary>
        /// Processes a collection of time-series entities.
        /// </summary>
        /// <param name="entities">Entities to be processed.</param>
        public virtual void ProcessEntities(IEnumerable<ITimeSeriesEntity> entities)
        {
        }

        /// <summary>
        /// Raises the <see cref="NewEntities"/> event.
        /// </summary>
        protected virtual void OnNewEntities(ICollection<ITimeSeriesEntity> entities)
        {
            try
            {
                lock (m_newEntitiesLock)
                {
                    if ((object)m_routingEventArgs == null)
                        m_routingEventArgs = new RoutingEventArgs();

                    m_routingEventArgs.TimeSeriesEntities = entities;

                    if ((object)NewEntities != null)
                        NewEntities(this, m_routingEventArgs);
                }

                IncrementProcessedEntities(entities.Count);
            }
            catch (Exception ex)
            {
                // We protect our code from consumer thrown exceptions
                OnProcessException(new InvalidOperationException(string.Format("Exception in consumer handler for NewEntities event: {0}", ex.Message), ex));
            }
        }

        /// <summary>
        /// Raises the <see cref="UnpublishedSamples"/> event.
        /// </summary>
        /// <param name="seconds">Total number of unpublished seconds of data.</param>
        protected virtual void OnUnpublishedSamples(int seconds)
        {
            try
            {
                if ((object)UnpublishedSamples != null)
                    UnpublishedSamples(this, new EventArgs<int>(seconds));
            }
            catch (Exception ex)
            {
                // We protect our code from consumer thrown exceptions
                OnProcessException(new InvalidOperationException(string.Format("Exception in consumer handler for UnpublishedSamples event: {0}", ex.Message), ex));
            }
        }

        #endregion
    }
}